<?php
/**
 * A collection of a single model class instances, stored in database.
 * By default, all stored instances belongs to the collection.
 * Acts as a lazy queryset. Implements Iterator interface.
 * @link http://www.martinfowler.com/eaaCatalog/repository.html
 *
 * uses standard MySQL library actually
 * @todo implement iterator http://fr.php.net/manual/fr/language.oop5.iterations.php
 * @link http://webappkit.net
 * @package webappkit
 * @subpackage db
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */
class DbCollection {

    /**
     * constructor
     */
    function DbCollection($class=null) {
        // setting stored class
        if (is_string($class))
            $this->class = $class;
        // auto-mapping on class / table ?
        // setting default filters
    }

    /**
     * sets owner mapper's reference
     * @param DbMapper $mapper
     */
    function setMapper(&$mapper) {
        $this->mapper = &$mapper;
        return true;
    }

    // queryset / filtering methods

    /**
     * register a model-based filter
     * checks filter's validity and converts its value for sql use
     * search for self string _filter{$key}($value) method
     * @param string $key name of the model property to filter on
     * @param mixed $value value to filter on
     * @return bool true if filter is supported and value is correct
     */
    function filter($key, $value) {
        $method = '_filter'.$key;
        if (method_exists($this,$method)) {
            // there is a custom validation / conversion method
            $sql_value = $this->$method($value);
        } else if (!array_key_exists($key, $this->filters)) {
            // unsupported filter, not listed is this->filters
            trigger_error("unsupported filter $key");
            return false;
        } else if (is_null($value) or (is_string($value) and strlen($value)==0)) {
            // value is null or empty string
            return false;
        } else {
            $sql_value = $this->_sqlValue($value);
        }
        $this->filters[$key] = $sql_value;
        return true;
    }

    /**
     * remove model-based, named filter
     * @access public
     * @param string $key name of the model property to filter on
     * @return bool
     */
    function removeFilter($key) {
        unset($this->filters[$key]);
        return true;
    }

    /**
     * chains filters grouped in array
     * @param array $filters associative array
     * @return DbCollection filtered instance
     */
    function filters($filters) {
        foreach ($filters as $key => $value) {
            $this->filter($key, $value);
        }
        return $this;
    }

    /**
     * adds a SQL WHERE clause filter
     * @param string $sql
     * /
    function sqlFilter($sql) {
        $this->filters[] = $sql;
    }/**/

    /**
     * return sql SELECT query
     * @return string
     * /
    function getSelectSql() {
        return $this->_buildQuery();
    }

    /**
     * return rows count in current selection
     * @return int
     */
    function count() {
        return mysql_num_rows($this->_getResult());
    }

    /**
     * get all items as an array
     * @access public
     * @return array(object)
     */
    function & getList() {
        // get result
        $list = array();
        while ($item = $this->_buildItem())
            $list[] = $item;
        return $list;
    }

    /**
     * gets a page of items
     * @param int $index (starting at 1)
     * @param int $lines lines per page, default 20
     * @return array(object)
     */
    function getPage($index=1, $lines=20) {
        // checking index
        // $index = is_int($index)?$index:1;
        // getting result
        $result = $this->_getResult();
        if ($index>1 and $this->count()>=($index*$lines))
            mysql_data_seek($result, ($index-1)*$lines);
        $list = array();
        for ($n=0; $n<$lines; $n++) {
            if (!$item = $this->_buildItem())
                break;
            $list[] = $item;
        }
        return $list;
    }

    /**
     * return pages count
     * @param int $lines lines per page
     * @return int
     */
    function countPages($lines=20) {
        return ceil($this->count() / $lines);
    }

    // insert / update / delete methods

    /**
     * inserts a single item into the collection
     * if objects implements iDbCollectionItem, sets generated id
     * @param object $item
     * @return mixed generated id
     */
    function insert($item) {
        // checking item's class
        if (!$this->_isObject($item)) {
            trigger_error("item's class is not {$this->class}");
            return false;
        }
        // building SQL query
        $sql = $this->_sqlInsert($item); // echo $sql; exit;
        // executing query
        if (!$this->_executeSql($sql))
            return false;
        // getting id
        $id=mysql_insert_id();
        $this->_setItemId($item, $id);
        return $id;
    }/**/

    /**
     * updates a single item into the collection
     * @access public
     * @param object $item instance to save
     * @return bool
     */
    function update($item) {
        // checking item's class
        if (!$this->_isObject($item)) {
            trigger_error("item's class is not {$this->class}");
            return false;
        }
        // building SQL query
        $sql = $this->_sqlUpdate($item);
        // executing query
        if (!$this->_executeSql($sql))
            return false;
        return true;
    }

    /**
     * deletes a single item
     * @param object $item object instance to delete
     * @return bool
     * /
    function delete($item) {
        if ($this->_isObject($item))
            $sql = $this->_buildDeleteItem($item);
        return $this->_executeSql($sql);
    }/**/

    /**
     * deletes a single item by key
     * @param scalar $id
     * /
    function deleteByKey($id) {
        $sql = $this->_buildDeleteByKey($id);
        return $this->_executeSql($sql);
    }/**/

    /**
     * deletes the current selection. BE CAREFUL !
     * @return int items deleted count
     * /
    function deleteAll() {
        $sql = $this->_buildDeleteAll(); // echo $sql; exit;
        if (!$this->_executeSql($sql)) {
            return false;
        }
        return mysql_affected_rows();
    }/**/

    // iterator methods

    /**
     * return current (first ) item in collection
     * @return object
     */
    function current() {
        if (!$this->current) {
            $this->current = $this->_buildItem();
        }
        return $this->current;
    }

    /**
     * return key of the current element (or 0)
     * @return scalar
     */
    function key() {

    }

    /**
     * set cursor to next element
     * @return bool
     */
    function next() {

    }/**/

    /**
     * tells if there is a next element in collection
     * @return bool
     */
    function valid() {

    }

    /**
     * resets cursor to first element
     * @return bool
     */
    function rewind() {

    }/**/

    // properties

    /**
     * @var string stored objects class
     * @access protected
     */
    var $class;

    /**
     * @var string main table
     */
    var $table;

    /**
     * @var string objects property being the primary key
     */
    var $class_key;

    /**
     * @var string table primary key
     */
    var $table_key;

    /**
     * @var array(name => sql value)
     */
    var $filters=array();

    /**
     * @var string ORDER BY clause
     */
    var $order;

    /**
     * @var resource query result as from mysql_query
     */
    var $result;

    /**
     * @var object current item
     */
    var $current;

    /**
     * @var array items already retrieved
     */
    // var $items=array();

    /**
     * @var DbMapper reference to owner DbMapper
     */
    var $mapper;

    // protected methods : instance generation etc

    /**
     * get query's result resource, execute if necessary
     * @access protected
     * @param bool $use_current if false, will execute query even if yet available
     * @return resource
     */
    function & _getResult($use_current=true) {
        if (!$this->result or !$use_current) {
            $sql = $this->_sqlSelect();
            $this->result = mysql_query($sql);
            if (!$this->result) {
                trigger_error(mysql_error($sql));
                trigger_error($sql);
            }
        }
        return $this->result;
    }

    /**
     * builds SQL SELECT query from stored parts
     * @access protected
     * @return string
     */
    function _sqlSelect() {
        $sql = $this->_sqlSelectFrom()
        .' '.$this->_sqlWhere()
        .' '.$this->_sqlOrder()
        .' limit 10000';
        return $sql;
    }

    /**
     * builds the SELECT+FROM statement
     * @return string
     */
    function _sqlSelectFrom() {
        // $vars = get_class_vars($this->class);
        return 'SELECT * FROM '.$this->table;
    }

    /**
     * builds a WHERE statement from stored filters
     * @access protected
     * @return string
     */
    function _sqlWhere() {
        $sql = ' WHERE 1';
        foreach ($this->filters as $key => $value)
            $sql .= " AND ($key = $value)";
        return $sql;
    }/**/

    /**
     * builds the ORDER BY statement
     */
    function _sqlOrder() {
        return '';
    }

    /**
     * builds INSERT query from an item
     * @param object $item
     * @return string SQL query
     * /
    function _buildInsert($item) {
        // searches for query template
        if (isset($this->sql_insert_tpl)) {
            $query = new QueryTpl($this->sql_insert_tpl);
        }
        return $query->render($item);
    }/**/

    /**
     * builds UPDATE query from an item
     * @access protected
     * @param object $item
     * @return string SQL query
     * /
    function _buildUpdate($item) {

    }/**/

    /**
     * builds DELETE query for a single item
     * @access protected
     * @param object $item
     * @return string SQL
     * /
    function _buildDelete($item) {
        if (!$class_key = $this->class_key)
            return false;
        return $this->_buildDeleteByKey($item->$class_key);
    }/**/

    /**
     * builds DELETE query by primary key
     * @access protected
     * @param scalar $id primary key's value
     * @return string SQL
     * /
    function _buildDeleteByKey($id) {
        $sql = 'DELETE FROM '.$this->table
        .' WHERE '.$this->table_key.'='.$this->_toSql($id)
        .' LIMIT 1';
        return $sql;
    }/**/

    /**
     * builds DELETE query from stored parts
     * @return string SQL query
     * /
    function _buildDeleteAll() {
        $sql = $this->sql_delete
        .' WHERE 1';
        foreach ($this->filters as $filter)
            $sql .= ' AND ('.$filter.')';
        return $sql;
    }/**/

    /**
     * builds a collection's item instance from current result
     * @uses $this->result
     * @return object|false
     */
    function _buildItem() {
        $result = $this->_getResult();
        // delegates to DbMapper if defined
        if ($this->mapper)
            return $this->mapper->buildObject($result, $this->class);
        // generic case
        return mysql_fetch_object($result, $this->class);
    }

    /**
     * sets item id
     * @param object $item
     * @param string $id
     */
    function _setItemId($item, $id) {
        $item->id = $id;
        return true;
    }

    /**
     * returns primary key filter from item
     */


    // utilities

    /**
     * protects and converts value for query insertion
     * @param mixed $value source
     * @param bool $enclose if true, adds ' around value
     * @return string
     */
    function _sqlValue($value, $enclose=false) {
        switch (true) {
            case is_bool($value):
                $value = $value?'1':'0';
                break;
            case is_int($value):
                $value=strval($value);
                break;
        }
        $value = mysql_escape_string($value);
        if ($enclose)
            $value = '\''.$value.'\'';
        return $value;
    }

    /**
     * executes a query
     * @access protected
     * @param string $sql SQL query
     * @return resource
     */
    function _executeSql($sql) {
        if (!$result=@mysql_query($sql)) {
            trigger_error(mysql_error());
            trigger_error($sql);
        }
        $this->last_sql = $sql;
        return $result;
    }

    /**
     * checks that is an object of the correct class
     * @access protected
     * @param mixed $object
     * @return bool
     */
    function _isObject($object) {
        return is_a($object, $this->class);
    }

    /**
     * return a string if filter is defined
     * @param string $key filter name
     * @param string $format as in sprintf(), use a string with %s
     * @return string
     */
    function _sqlFilter($key, $format) {
        if (!is_string($this->filters[$key]))
            return '';
        return sprintf($format, $this->filters[$key]);
    }
}